﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Text;

namespace Server.Packets
{
    /// <summary>
    /// Represents a packet of data sent over the TCP connection.
    /// </summary>
    [ContractClass(typeof(PacketContracts))]
    public abstract class Packet
    {
        #region Fields

        /// <summary>
        /// Represents a <see cref="T:System.Collections.Generic.Dictionary`2" /> of the packets constructors.
        /// </summary>
        private static Dictionary<byte, Func<object>> m_constructors = new Dictionary<byte, Func<object>>();

        #endregion

        #region Properties

        /// <summary>
        /// Identifier.
        /// </summary>
        protected abstract byte Id { get; }

        /// <summary>
        /// Define the specified <see cref="T:Server.Packets.Packet" /> list.
        /// </summary>
        /// <param name="packets">The <see cref="T:Server.Packets.Packet" /> list.</param>
        public static void Define(params Packet[] packets)
        {
            Contract.Requires<ArgumentNullException>(packets != null);

            foreach (var packet in packets)
                m_constructors[packet.Id] = packet.GetType().CreateDefaultConstructor();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Reads the raw packet data from the data reader.
        /// </summary>
        /// <param name="reader">Data reader.</param>
        /// <exception cref="T:System.IO.EndOfStreamException">The end of the stream is reached.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The stream is closed.</exception>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="T:System.InvalidOperationException">Unknown packet id -or- the packet could not be instanced.</exception>
        internal static Packet ReadData(BinaryReader reader)
        {
            Contract.Requires<ArgumentNullException>(reader != null);
            Contract.Ensures(Contract.Result<Packet>() != null);

            var packet = GetNewPacket(reader.ReadByte());
            packet.ReadPacketData(reader);
            return packet;
        }

        /// <summary>
        /// Write the raw packet data, encoded with a specific format, into a <see cref="T:System.Byte" /> array.
        /// </summary>
        /// <param name="encoding">The <see cref="T:System.Text.Encoding" /> format.</param>
        /// <returns>A <see cref="T:System.Byte" /> array containing the raw packet data encoded to the given format.</returns>
        internal byte[] WriteData(Encoding encoding)
        {
            Contract.Requires<ArgumentNullException>(encoding != null);
            Contract.Ensures(Contract.Result<byte[]>() != null);

            MemoryStream memoryStream;
            using (var writer = new BinaryWriter((memoryStream = new MemoryStream()), encoding))
            {
                WriteData(writer);
                return memoryStream.ToArray();
            }
        }

        /// <summary>
        /// Reads the raw packet data from a data reader.
        /// </summary>
        /// <param name="reader">Data reader.</param>
        protected abstract void ReadPacketData(BinaryReader reader);

        /// <summary>
        /// Write the raw packet data to a data writer.
        /// </summary>
        /// <param name="writer">Data writer.</param>
        protected abstract void WritePacketData(BinaryWriter writer);

        /// <summary>
        /// Write the raw packet data to a data writer.
        /// </summary>
        /// <param name="writer">Data writer.</param>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The stream is closed.</exception>
        private void WriteData(BinaryWriter writer)
        {
            Contract.Requires<ArgumentNullException>(writer != null);

            writer.Write(Id);
            WritePacketData(writer);
        }

        /// <summary>
        /// Get a new instance of a <see cref="T:Server.Packets.Packet" /> given its identifier.
        /// </summary>
        /// <param name="id">The packet identifier.</param>
        /// <returns>The instanced <see cref="T:Server.Packets.Packet" />.</returns>
        /// <exception cref="T:System.InvalidOperationException">Unknown packet id -or- the packet could not be instanced.</exception>
        private static Packet GetNewPacket(byte id)
        {
            Contract.Ensures(Contract.Result<Packet>() != null);

            Func<object> constructor;
            if (m_constructors.TryGetValue(id, out constructor) && constructor != null)
            {
                var packet = constructor.Invoke() as Packet;
                if (packet == null)
                    throw new InvalidOperationException(String.Format("The packet with id '{0}' could not be instanced.", id));
                return packet;
            }
            // unknown packet Id
            throw new InvalidOperationException(String.Format("Unknown packet id : {0}", id));
        }

        #endregion
    }
}
